﻿using System;
using System.Diagnostics;
using SfsTools.Core;
using mindvnas.Common;
using System.IO;

namespace SfsTools.Copy
{
    /// <summary>
    /// Implements the <em>copy</em> application.
    /// </summary>
    public class Program : ConsoleBase
    {
        #region Properties

        /// <summary>
        /// Contains the path to the image file to open.
        /// </summary>
        private String FImageFile;
        /// <summary>
        /// Contains the source path.
        /// </summary>
        private String FSource;
        /// <summary>
        /// Contains the destination path.
        /// </summary>
        private String FTarget;
        /// <summary>
        /// Contains the current task.
        /// </summary>
        private Task FCurrentTask;
        /// <summary>
        /// Contains the list of copy tasks to perform.
        /// </summary>
        private readonly System.Collections.Generic.List<Task> FTasks = new System.Collections.Generic.List<Task>();
        /// <summary>
        /// Contains the prefix for image paths.
        /// </summary>
        private static readonly String FSfsPrefix = Translation.Resources.GetString("SP");
        /// <summary>
        /// Contains the prefix for local paths.
        /// </summary>
        private static readonly String FLocalPrefix = Translation.Resources.GetString("LP");

        #endregion

        #region Methods

        #region Entry point
        /// <summary>
        /// Entry point.
        /// </summary>
        /// <param name="arguments">The command-line arguments.</param>
        [DebuggerStepThrough]
        static void Main(String[] arguments)
        {
#if DEBUG
            System.Collections.Generic.List<String> args = new System.Collections.Generic.List<String>();
            //args.Add(@"-src=file://e:\Work\VisualStudio\SfsTools\Core\exceptions.txt");
            args.Add(@"-src=sfs://exceptions.txt");
            //args.Add(@"-dst=sfs://exceptions2.txt");
            args.Add(@"-dst=file://e:\Work\VisualStudio\SfsTools\Core\exceptions2.txt");
            //arguments = args.ToArray();
#endif
            new Program().Process(arguments);
        }
        #endregion

        #region Arguments

        /// <summary>
        /// Invoked before processing arguments.
        /// </summary>
        protected override void OnBeforeProcessArguments()
        {
            FTarget = null;
            FSource = null;
        }

        /// <summary>
        /// Invoked after processing arguments.
        /// </summary>
        protected override void OnAfterProcessArguments()
        {
            CheckIfBothPathsSet();
        }

        #region Check for source and target path

        /// <summary>
        /// Check if both source and destination path is set, and decide copy mode based on file specifiers.
        /// </summary>
        [DebuggerStepThrough]
        private void CheckIfBothPathsSet()
        {
            if (String.IsNullOrEmpty(FSource) || String.IsNullOrEmpty(FTarget))
                return;
            if (FSource.StartsWith(FSfsPrefix))
            {
                // Image to...
                if (FTarget.StartsWith(FSfsPrefix))
                {
                    // Image to image.
                    Task task = new CopyImageToImageTask();
                    task.Source = FSource.Substring(FSfsPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    task.Target = FTarget.Substring(FSfsPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    FTasks.Add(task);
                }
                else if (FTarget.StartsWith(FLocalPrefix))
                {
                    // Image to local.
                    Task task = new CopyImageToLocalTask();
                    task.Source = FSource.Substring(FSfsPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    task.Target = FTarget.Substring(FLocalPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    FTasks.Add(task);
                }
                else
                {
                    // Image to local, no prefix.
                    Task task = new CopyImageToLocalTask();
                    task.Source = FSource.Substring(FSfsPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    task.Target = FTarget.Replace('/', Path.DirectorySeparatorChar);
                    FTasks.Add(task);
                }
            }
            else if (FSource.StartsWith(FLocalPrefix))
            {
                // Local to...
                if (FTarget.StartsWith(FSfsPrefix))
                {
                    // Local to image.
                    Task task = new CopyLocalToImageTask();
                    task.Source = FSource.Substring(FLocalPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    task.Target = FTarget.Substring(FSfsPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    FTasks.Add(task);
                }
                else if (FTarget.StartsWith(FLocalPrefix))
                {
                    // Local to local.
                    Task task = new CopyLocalToLocalTask();
                    task.Source = FSource.Substring(FLocalPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    task.Target = FTarget.Substring(FLocalPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    FTasks.Add(task);
                }
                else
                {
                    // Local to local, no prefix.
                    Task task = new CopyLocalToLocalTask();
                    task.Source = FSource.Substring(FLocalPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    task.Target = FTarget.Replace('/', Path.DirectorySeparatorChar);
                    FTasks.Add(task);
                }
            }
            else
            {
                // Local to... (no prefix)
                if (FTarget.StartsWith(FSfsPrefix))
                {
                    // Local to image.
                    Task task = new CopyLocalToImageTask();
                    task.Source = FSource.Replace('/', Path.DirectorySeparatorChar);
                    task.Target = FTarget.Substring(FSfsPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    FTasks.Add(task);
                }
                else if (FTarget.StartsWith(FLocalPrefix))
                {
                    // Local to local.
                    Task task = new CopyLocalToLocalTask();
                    task.Source = FSource.Replace('/', Path.DirectorySeparatorChar);
                    task.Target = FTarget.Substring(FLocalPrefix.Length).Replace('/', Path.DirectorySeparatorChar);
                    FTasks.Add(task);
                }
                else
                {
                    // Local to local, no prefix.
                    Task task = new CopyLocalToLocalTask();
                    task.Source = FSource.Replace('/', Path.DirectorySeparatorChar);
                    task.Target = FTarget.Replace('/', Path.DirectorySeparatorChar);
                    FTasks.Add(task);
                }
            }
            FSource = FTarget = null;
        }

        #endregion

        #region Check if task completely specified

        /// <summary>
        /// Check if both source and destination is set on the current task.
        /// </summary>
        [DebuggerStepThrough]
        private void CheckIfTaskSpecified()
        {
            if ((FCurrentTask.Source != null) && (FCurrentTask.Target != null))
            {
                FTasks.Add(FCurrentTask);
                FCurrentTask = null;
            }
        }

        #endregion

        /// <summary>
        /// Processes the specified command-line argument.
        /// </summary>
        /// <param name="argument">The command-line argument.</param>
        protected override void ProcessArgument(ConsoleBaseArgument argument)
        {
            if (argument.Name == "-image")
            {
                FImageFile = argument.Value;
                Arguments.Add(argument);
            }
            else if (argument.Name == "-dst")
            {
                if (FCurrentTask != null)
                {
                    FCurrentTask.Target = argument.Value;
                    CheckIfTaskSpecified();
                }
                else
                {
                    FTarget = argument.Value;
                    CheckIfBothPathsSet();
                }
            }
            else if (argument.Name == "-src")
            {
                if (FCurrentTask != null)
                {
                    FCurrentTask.Source = argument.Value;
                    CheckIfTaskSpecified();
                }
                else
                {
                    FSource = argument.Value;
                    CheckIfBothPathsSet();
                }
            }
            else if (argument.Name == "-l2l")
            {
                FCurrentTask = new CopyLocalToLocalTask();
                FSource = FTarget = null;
            }
            else if (argument.Name == "-l2s")
            {
                FCurrentTask = new CopyLocalToImageTask();
                FSource = FTarget = null;
            }
            else if (argument.Name == "-l2i")
            {
                FCurrentTask = new CopyLocalToImageTask();
                FSource = FTarget = null;
            }
            else if (argument.Name == "-s2l")
            {
                FCurrentTask = new CopyImageToLocalTask();
                FSource = FTarget = null;
            }
            else if (argument.Name == "-s2s")
            {
                FCurrentTask = new CopyImageToImageTask();
                FSource = FTarget = null;
            }
            else if (argument.Name == "-i2l")
            {
                FCurrentTask = new CopyImageToLocalTask();
                FSource = FTarget = null;
            }
            else if (argument.Name == "-i2i")
            {
                FCurrentTask = new CopyImageToImageTask();
                FSource = FTarget = null;
            }
            else
            {
                base.ProcessArgument(argument);
            }
        }

        #region Help

        /// <summary>
        /// Displays an usage notice.
        /// </summary>
        [DebuggerStepThrough]
        protected override void UsageParameters()
        {
            base.UsageParameters();
            //
            UsageParameter("-image", Translation.Resources.GetString("ArgI"));
            UsageParameter("-src", Translation.Resources.GetString("ArgSrc"));
            UsageParameter("-dst", Translation.Resources.GetString("ArgDst"));
            UsageParameter("-l2l", Translation.Resources.GetString("ArgL2L"));
            UsageParameter("-l2s", Translation.Resources.GetString("ArgL2S"));
            UsageParameter("-s2l", Translation.Resources.GetString("ArgS2L"));
            UsageParameter("-s2s", Translation.Resources.GetString("ArgS2S"));
            WriteLine();
            WriteLine(Translation.Resources.GetString("H1"));
            WriteLine(Translation.Resources.GetString("H2"));
            WriteLine();
            WriteLine(Translation.Resources.GetString("H3"));
            WriteLine(Translation.Resources.GetString("H4"));
            WriteLine(Translation.Resources.GetString("H5"));
        }

        #endregion

        #endregion

        /// <summary>
        /// Performs whatever the application does.
        /// </summary>
        protected override void ProcessCore()
        {
#if DEBUG
            String pathToVolume = @"E:\Work\VisualStudio\SfsTools\Core\";
            FImageFile = pathToVolume + "disk";
#endif
            if (HelpRequested || FImageFile == null || FTasks.Count == 0)
            {
                HelpRequested = true;
                base.ProcessCore();
            }
            else
            {
                using (FileBackedVolume volume = FileBackedVolume.Open(FImageFile))
                {
                    using (TransactionalVolume trv = new TransactionalVolume(volume))
                    {
                        using (FileSystem fs = new FileSystem(trv))
                        {
                            // Execute tasks.
                            foreach (Task task in FTasks)
                            {
                                task.Execute(fs);
                            }
                            // Commit changes.
                            trv.Commit();
                        }
                    }
                }
#if DEBUG
                Console.ReadLine();
#endif
            }
        }

        #endregion

        #region Constructors
        /// <overloads>Initializes a new instance of <see cref="Program"/>.</overloads>
        /// <summary>
        /// Initializes a new instance of <see cref="Program"/>.
        /// </summary>
        [DebuggerStepThrough]
        public Program()
            : base()
        {
        }
        #endregion

        #region Helper classes

        #region Task

        /// <summary>
        /// Represents a task of copying one file or directory.
        /// </summary>
        private abstract class Task
        {
            #region Properties

            /// <summary>
            /// Contains the source path.
            /// </summary>
            private String FSource;
            /// <summary>
            /// Gets or sets the source path.
            /// </summary>
            /// <exception cref="ArgumentNullException">The argument <paramref name="value"/> is <see langword="null"/>.</exception>
            public String Source
            {
                [DebuggerStepThrough]
                get
                {
                    return FSource;
                }
                [DebuggerStepThrough]
                set
                {
                    if (value == null)
                        throw new ArgumentNullException("value");
                    FSource = value;
                }
            }

            /// <summary>
            /// Contains the destination path.
            /// </summary>
            private String FTarget;
            /// <summary>
            /// Gets or sets the destination path.
            /// </summary>
            /// <exception cref="ArgumentNullException">The argument <paramref name="value"/> is <see langword="null"/>.</exception>
            public String Target
            {
                [DebuggerStepThrough]
                get
                {
                    return FTarget;
                }
                [DebuggerStepThrough]
                set
                {
                    if (value == null)
                        throw new ArgumentNullException("value");
                    FTarget = value;
                }
            }

            #endregion

            #region Methods

            /// <summary>
            /// Executes the task.
            /// </summary>
            /// <param name="fs">The file system to execute the task on.</param>
            /// <exception cref="System.IO.IOException">An IO error occurred.</exception>
            [DebuggerStepThrough]
            public void Execute(FileSystem fs)
            {
                try
                {
                    if (Source == Target)
                    {
                        Console.WriteLine(Translation.Resources.GetString(new String('C', 1)));
                    }
                    else
                    {
                        ExecuteInternal(fs);
                    }
                }
                catch (FileNotFoundException ex)
                {
                    Console.WriteLine(ex.Message);
                }
                catch (DirectoryNotFoundException ex)
                {
                    Console.WriteLine(ex.Message);
                }
                catch (PathNotFoundException ex)
                {
                    Console.WriteLine(ex.Message);
                }
            }

            /// <summary>
            /// Executes the task.
            /// </summary>
            /// <param name="fs">The file system to execute the task on.</param>
            /// <exception cref="IOException">An IO error occurred.</exception>
            protected abstract void ExecuteInternal(FileSystem fs);

            /// <summary>
            /// Opens the specified file on the filesystem for reading.
            /// </summary>
            /// <param name="fs">The file system.</param>
            /// <param name="path">The path to the file.</param>
            /// <returns>A <see cref="System.IO.Stream"/> that can be used to read to the file.</returns>
            /// <exception cref="PathNotFoundException">The specified path cannot be found.</exception>
            /// <exception cref="DirectoryNotFoundException">The specified path, or parts thereof, cannot be found.</exception>
            /// <exception cref="IOException">Some other IO error occurred.</exception>
            protected static Stream OpenFile(FileSystem fs, String path)
            {
                IDirectory root = fs.RootDirectory;
                // Will throw a PathNotFoundException if the path doesnt exist - this is acceptable.
                IFileSystemObject fso = root.Resolve(path);
                IFile file = fso as IFile;
                if (file == null)
                {
                    // The path represents a directory.
                    throw new PathDoesNotSpecifyFileException();
                }
                else
                {
                    // File already exists.
                    return file.OpenRead();
                }
            }

            /// <summary>
            /// Opens the specified file on the filesystem for writing.
            /// </summary>
            /// <param name="fs">The file system.</param>
            /// <param name="path">The path to the file.</param>
            /// <param name="length">The size of the file to create.</param>
            /// <returns>A <see cref="System.IO.Stream"/> that can be used to write to the file.</returns>
            /// <exception cref="PathNotFoundException">The specified path cannot be found.</exception>
            /// <exception cref="IndexAreaFullException">The index area is full.</exception>
            /// <exception cref="DataAreaFullException">The data area is full.</exception>
            /// <exception cref="System.IO.DirectoryNotFoundException">The specified path, or parts thereof, cannot be found.</exception>
            /// <exception cref="System.IO.IOException">Some other IO error occurred.</exception>
            protected static Stream OpenOrCreateFile(FileSystem fs, String path, long length)
            {
                IDirectory root = fs.RootDirectory;
                IFileSystemObject fso;
                try
                {
                    // Will throw a PathNotFoundException if the path doesnt exist - the path needs checking.
                    fso = root.Resolve(path);
                    IFile file = fso as IFile;
                    if (file == null)
                    {
                        // The path represents a directory.
                        throw new PathDoesNotSpecifyFileException();
                    }
                    else
                    {
                        // File already exists.
                        return file.OpenWrite();
                    }
                }
                catch (PathNotFoundException)
                {
                    // File or path to file doesnt exist.
                    int index = path.LastIndexOf(Path.DirectorySeparatorChar);
                    if (index <= 0)
                    {
                        // Create file in root directory.
                        if (index < 0)
                            return root.CreateFile(path, length).OpenWrite();
                        else
                            return root.CreateFile(path.Substring(1), length).OpenWrite();
                    }
                    else
                    {
                        // Resolve this directory.
                        String dirPath = path.Substring(0, index - 1);
                        // Will throw a PathNotFoundException if the path doesnt exist - this is acceptable.
                        fso = root.Resolve(dirPath);
                        IDirectory dir = fso as IDirectory;
                        if (dir == null)
                        {
                            // Path represents a file...
                            throw new PathNotFoundException(path);
                        }
                        else
                        {
                            // Create file in subdirectory.
                            String name = path.Substring(index + 1);
                            return dir.CreateFile(name, length).OpenWrite();
                        }
                    }
                }
            }

            /// <summary>
            /// Copies the contents of <paramref name="src"/> to <paramref name="dst"/>.
            /// </summary>
            /// <param name="src">The source stream.</param>
            /// <param name="dst">The target stream.</param>
            /// <exception cref="System.IO.IOException">An IO error occurred.</exception>
            protected static void Copy(Stream src, Stream dst)
            {
                byte[] buffer = new byte[4096];
                try
                {
                    for (; ; )
                    {
                        int read = src.Read(buffer, 0, buffer.Length);
                        if (read == 0)
                            break;
                        dst.Write(buffer, 0, read);
                    }
                }
                catch (EndOfStreamException)
                {
                    // Ignore.
                }
            }

            #endregion

            #region Constructors
            /// <overloads>Initializes a new instance of <see cref="Task"/>.</overloads>
            /// <summary>
            /// Initializes a new instance of <see cref="Task"/>.
            /// </summary>
            [DebuggerStepThrough]
            protected Task()
                : base()
            {
            }
            #endregion
        }

        #endregion

        #region Local to image

        /// <summary>
        /// Represents a task of copying a file or directory from the local file system to the SFS file system.
        /// </summary>
        private class CopyLocalToImageTask : Task
        {
            #region Methods

            /// <summary>
            /// Executes the task.
            /// </summary>
            /// <param name="fs">The file system to execute the task on.</param>
            /// <exception cref="System.IO.IOException">An IO error occurred.</exception>
            protected override void ExecuteInternal(FileSystem fs)
            {
                using (Stream src = new FileStream(Source, FileMode.Open, FileAccess.Read))
                {
                    using (Stream dst = OpenOrCreateFile(fs, Target, src.Length))
                    {
                        Copy(src, dst);
                    }
                }
            }

            #endregion

            #region Constructors
            /// <overloads>Initializes a new instance of <see cref="CopyLocalToImageTask"/>.</overloads>
            /// <summary>
            /// Initializes a new instance of <see cref="CopyLocalToImageTask"/>.
            /// </summary>
            [DebuggerStepThrough]
            public CopyLocalToImageTask()
                : base()
            {
            }
            #endregion
        }

        #endregion

        #region Local to local

        /// <summary>
        /// Represents a task of copying a file or directory on the local file system.
        /// </summary>
        private class CopyLocalToLocalTask : Task
        {
            #region Methods

            /// <summary>
            /// Executes the task.
            /// </summary>
            /// <param name="fs">The file system to execute the task on.</param>
            /// <exception cref="System.IO.IOException">An IO error occurred.</exception>
            protected override void ExecuteInternal(FileSystem fs)
            {
                using (Stream src = new FileStream(Source, FileMode.Open, FileAccess.Read))
                {
                    using (Stream dst = new FileStream(Target, FileMode.Create, FileAccess.Write, FileShare.Read))
                    {
                        Copy(src, dst);
                    }
                }
            }

            #endregion

            #region Constructors
            /// <overloads>Initializes a new instance of <see cref="CopyLocalToLocalTask"/>.</overloads>
            /// <summary>
            /// Initializes a new instance of <see cref="CopyLocalToLocalTask"/>.
            /// </summary>
            [DebuggerStepThrough]
            public CopyLocalToLocalTask()
                : base()
            {
            }
            #endregion
        }

        #endregion

        #region Image to local

        /// <summary>
        /// Represents a task of copying a file or directory from the SFS file system to the local file system.
        /// </summary>
        private class CopyImageToLocalTask : Task
        {
            #region Methods

            /// <summary>
            /// Executes the task.
            /// </summary>
            /// <param name="fs">The file system to execute the task on.</param>
            /// <exception cref="System.IO.IOException">An IO error occurred.</exception>
            protected override void ExecuteInternal(FileSystem fs)
            {
                using (Stream src = OpenFile(fs, Source))
                {
                    using (Stream dst = new FileStream(Target, FileMode.Create, FileAccess.Write, FileShare.Read))
                    {
                        Copy(src, dst);
                    }
                }
            }

            #endregion

            #region Constructors
            /// <overloads>Initializes a new instance of <see cref="CopyImageToLocalTask"/>.</overloads>
            /// <summary>
            /// Initializes a new instance of <see cref="CopyImageToLocalTask"/>.
            /// </summary>
            [DebuggerStepThrough]
            public CopyImageToLocalTask()
                : base()
            {
            }
            #endregion
        }

        #endregion

        #region Image to image

        /// <summary>
        /// Represents a task of copying a file or directory on the SFS file system.
        /// </summary>
        private class CopyImageToImageTask : Task
        {
            #region Methods

            /// <summary>
            /// Executes the task.
            /// </summary>
            /// <param name="fs">The file system to execute the task on.</param>
            /// <exception cref="System.IO.IOException">An IO error occurred.</exception>
            protected override void ExecuteInternal(FileSystem fs)
            {
                using (Stream src = OpenFile(fs, Source))
                {
                    using (Stream dst = OpenOrCreateFile(fs, Target, src.Length))
                    {
                        Copy(src, dst);
                    }
                }
            }

            #endregion

            #region Constructors
            /// <overloads>Initializes a new instance of <see cref="CopyImageToImageTask"/>.</overloads>
            /// <summary>
            /// Initializes a new instance of <see cref="CopyImageToImageTask"/>.
            /// </summary>
            [DebuggerStepThrough]
            public CopyImageToImageTask()
                : base()
            {
            }
            #endregion
        }

        #endregion

        #endregion
    }
}
